/* * Copyright (C) 2015 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package com.android.talkback.controller; import static org.junit.Assert.assertNotEquals; import com.google.android.marvin.talkback.TalkBackService; import android.app.Activity; import android.os.Build; import android.os.Bundle; import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat.CollectionItemInfoCompat; import android.test.suitebuilder.annotation.MediumTest; import android.util.Log; import android.view.View; import android.view.accessibility.AccessibilityNodeInfo; import android.webkit.WebView; import android.widget.HorizontalScrollView; import android.widget.ListView; import android.widget.TextView; import com.android.talkback.CursorGranularity; import com.android.talkback.InputModeManager; import com.android.talkback.R; import com.android.talkback.Utterance; import com.android.utils.AccessibilityNodeInfoUtils; import com.android.utils.traversal.TraversalStrategy; import com.googlecode.eyesfree.testing.CharSequenceFilter; import com.googlecode.eyesfree.testing.TalkBackInstrumentationTestCase; import com.googlecode.eyesfree.testing.UtteranceFilter; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.List; public class CursorControllerAppTest extends TalkBackInstrumentationTestCase { private TalkBackService mTalkBack; private CursorController mCursorController; private List <AccessibilityNodeInfoCompat> mObtainedNodes; @Override public void setUp() throws Exception { super.setUp(); mTalkBack = getService(); mCursorController = mTalkBack.getCursorController(); mObtainedNodes = new ArrayList<>(); // We don't want to include the action bar in our traversals. final Activity activity = getActivity(); getInstrumentation().runOnMainSync(new Runnable() { @Override public void run() { activity.getActionBar().hide(); } }); getInstrumentation().waitForIdleSync(); } @Override public void tearDown() throws Exception { super.tearDown(); AccessibilityNodeInfoUtils.recycleNodes(mObtainedNodes); } @MediumTest public void testGetSetCursor() { setContentView(R.layout.cursor_test); AccessibilityNodeInfoCompat squareButton = getNodeForId(R.id.button_square); mCursorController.setCursor(squareButton); waitForAccessibilityIdleSync(); assertEquals(squareButton, mCursorController.getCursor()); } @MediumTest public void testGetCursorOrInputCursor() { setContentView(R.layout.text_activity); AccessibilityNodeInfoCompat usernameEditText = getNodeForId(R.id.username); mCursorController.setCursor(usernameEditText); waitForAccessibilityIdleSync(); // Click to open the keyboard. mCursorController.clickCurrent(); waitForAccessibilityIdleSync(); // Remove accessibility focus from the text field. Input focus should stay, however. mCursorController.clearCursor(); waitForAccessibilityIdleSync(); assertEquals(usernameEditText, mCursorController.getCursorOrInputCursor()); } @MediumTest public void testClearCursor() { setContentView(R.layout.cursor_test); AccessibilityNodeInfoCompat squareButton = getNodeForId(R.id.button_square); mCursorController.setCursor(squareButton); waitForAccessibilityIdleSync(); mCursorController.clearCursor(); waitForAccessibilityIdleSync(); assertFalse(squareButton.equals(mCursorController.getCursor())); } @MediumTest public void testRefocus() { setContentView(R.layout.cursor_test); AccessibilityNodeInfoCompat squareButton = getNodeForId(R.id.button_square); mCursorController.setCursor(squareButton); waitForAccessibilityIdleSync(); mCursorController.refocus(); waitForAccessibilityIdleSync(); assertEquals(squareButton, mCursorController.getCursor()); } @MediumTest public void testClickCurrent() { setContentView(R.layout.cursor_test); AccessibilityNodeInfoCompat checkBox = getNodeForId(R.id.check_me); assertFalse(checkBox.isChecked()); mCursorController.setCursor(checkBox); waitForAccessibilityIdleSync(); mCursorController.clickCurrent(); waitForAccessibilityIdleSync(); checkBox.refresh(); assertTrue(checkBox.isChecked()); } @MediumTest public void testMore() { setContentView(R.layout.cursor_test); AccessibilityNodeInfoCompat teamsList = getNodeForId(R.id.teams_list); AccessibilityNodeInfoCompat oldTeam = teamsList.getChild(0); CollectionItemInfoCompat oldItemInfo = oldTeam.getCollectionItemInfo(); assertEquals(0, oldItemInfo.getRowIndex()); mCursorController.setCursor(oldTeam); // Put cursor in the list view so that it will scroll. waitForAccessibilityIdleSync(); mCursorController.more(); waitForAccessibilityIdleSync(); // The first item in the list should have scrolled out of view. The first visible item in // the list should now be something different. AccessibilityNodeInfoCompat newTeam = teamsList.getChild(0); CollectionItemInfoCompat newItemInfo = newTeam.getCollectionItemInfo(); assertNotEquals(0, newItemInfo.getRowIndex()); } @MediumTest public void testLess() { setContentView(R.layout.cursor_test); final ListView teamsListView = (ListView) getViewForId(R.id.teams_list); final int lastTeamIndex = teamsListView.getCount() - 1; // Scroll the list view down to display the last item before beginning the test. getInstrumentation().runOnMainSync(new Runnable() { @Override public void run() { teamsListView.setSelection(lastTeamIndex); } }); getInstrumentation().waitForIdleSync(); waitForAccessibilityIdleSync(); AccessibilityNodeInfoCompat teamsList = getNodeForId(R.id.teams_list); AccessibilityNodeInfoCompat oldTeam = teamsList.getChild(teamsList.getChildCount() - 1); CollectionItemInfoCompat oldItemInfo = oldTeam.getCollectionItemInfo(); assertEquals(lastTeamIndex, oldItemInfo.getRowIndex()); mCursorController.setCursor(oldTeam); // Put cursor in the list view so that it will scroll. waitForAccessibilityIdleSync(); mCursorController.less(); waitForAccessibilityIdleSync(); // The last item in the list should have scrolled out of view. The last visible item // should be something different. teamsList.refresh(); // We need to update the list view's child count. AccessibilityNodeInfoCompat newTeam = teamsList.getChild(teamsList.getChildCount() - 1); CollectionItemInfoCompat newItemInfo = newTeam.getCollectionItemInfo(); assertNotEquals(lastTeamIndex, newItemInfo.getRowIndex()); } @MediumTest public void testJumpToTop() { setContentView(R.layout.cursor_test); AccessibilityNodeInfoCompat firstLabel = getNodeForId(R.id.text_first_item); mCursorController.jumpToTop(InputModeManager.INPUT_MODE_TOUCH); waitForAccessibilityIdleSync(); AccessibilityNodeInfoCompat cursor = mCursorController.getCursor(); assertEquals(firstLabel, cursor); } @MediumTest public void testJumpToBottom() { setContentView(R.layout.cursor_test); AccessibilityNodeInfoCompat lastLabel = getNodeForId(R.id.text_last_item); mCursorController.jumpToBottom(InputModeManager.INPUT_MODE_TOUCH); waitForAccessibilityIdleSync(); AccessibilityNodeInfoCompat cursor = mCursorController.getCursor(); assertEquals(lastLabel, cursor); } @MediumTest public void testNext_noWrap_noScroll_noUseInputFocus() { setContentView(R.layout.cursor_test); AccessibilityNodeInfoCompat teamsList = getNodeForId(R.id.teams_list); AccessibilityNodeInfoCompat firstLabel = getNodeForId(R.id.text_first_item); AccessibilityNodeInfoCompat squareButton = getNodeForId(R.id.button_square); AccessibilityNodeInfoCompat checkBox = getNodeForId(R.id.check_me); AccessibilityNodeInfoCompat lastLabel = getNodeForId(R.id.text_last_item); mCursorController.setCursor(firstLabel); waitForAccessibilityIdleSync(); // This is the expected traversal: // (Initial focus) text_first_item // 1. button_square // 2. check_me // 3-N. teams_list - visible items only // N. text_last_item final int teamsVisibleCount = teamsList.getChildCount(); final int traversals = 3 + teamsVisibleCount; // 3 non-list items + N visible list items. List<AccessibilityNodeInfoCompat> nodes = navigate(TraversalStrategy.SEARCH_FOCUS_FORWARD, traversals, false, false, false); assertEquals(squareButton, nodes.get(0)); assertEquals(checkBox, nodes.get(1)); assertHasParent(teamsList, nodes.subList(2, traversals - 1)); assertEquals(lastLabel, nodes.get(traversals - 1)); } @MediumTest public void testNext_noWrap_doScroll_noUseInputFocus() { setContentView(R.layout.cursor_test); AccessibilityNodeInfoCompat teamsList = getNodeForId(R.id.teams_list); AccessibilityNodeInfoCompat checkBox = getNodeForId(R.id.check_me); AccessibilityNodeInfoCompat lastLabel = getNodeForId(R.id.text_last_item); mCursorController.setCursor(checkBox); waitForAccessibilityIdleSync(); // This is the expected traversal: // (Initial focus) check_me // 1-9. teams_list - all items // 10. text_last_item final int traversals = 10; List<AccessibilityNodeInfoCompat> nodes = navigate(TraversalStrategy.SEARCH_FOCUS_FORWARD, traversals, false, true, false); assertHasParent(teamsList, nodes.subList(0, traversals - 1)); assertEquals(lastLabel, nodes.get(traversals - 1)); } @MediumTest public void testNext_doWrap_noScroll_noUseInputFocus() { setContentView(R.layout.cursor_test); AccessibilityNodeInfoCompat lastLabel = getNodeForId(R.id.text_last_item); AccessibilityNodeInfoCompat firstLabel = getNodeForId(R.id.text_first_item); mCursorController.setCursor(lastLabel); waitForAccessibilityIdleSync(); // This is the expected traversal: // (Initial focus) text_last_item // 1. text_last_item - first next() pauses on same node // 2. text_first_item - second next() should wrap around final int traversals = 2; List<AccessibilityNodeInfoCompat> nodes = navigate(TraversalStrategy.SEARCH_FOCUS_FORWARD, traversals, true, false, false); assertEquals(lastLabel, nodes.get(0)); assertEquals(firstLabel, nodes.get(1)); } @MediumTest public void testNavigateWithGranularity() { setContentView(R.layout.cursor_test); // Focus to the beginning of page. mCursorController.setCursor(getNodeForId(R.id.text_first_item)); waitForAccessibilityIdleSync(); // Navigate forward by word. startRecordingUtterances(); assertTrue(mCursorController.nextWithSpecifiedGranularity(CursorGranularity.WORD, false /* shouldWrap */, true /* shouldScroll */, true /* useInputFocusAsPivotIfEmpty */, InputModeManager.INPUT_MODE_TOUCH)); waitForAccessibilityIdleSync(); stopRecordingAndAssertUtterance("Beginning"); startRecordingUtterances(); assertTrue(mCursorController.nextWithSpecifiedGranularity(CursorGranularity.WORD, false /* shouldWrap */, true /* shouldScroll */, true /* useInputFocusAsPivotIfEmpty */, InputModeManager.INPUT_MODE_TOUCH)); waitForAccessibilityIdleSync(); stopRecordingAndAssertUtterance("of"); startRecordingUtterances(); assertTrue(mCursorController.nextWithSpecifiedGranularity(CursorGranularity.WORD, false /* shouldWrap */, true /* shouldScroll */, true /* useInputFocusAsPivotIfEmpty */, InputModeManager.INPUT_MODE_TOUCH)); waitForAccessibilityIdleSync(); stopRecordingAndAssertUtterance("page"); // Returns false if user tries to go out from the current focused item. assertFalse(mCursorController.nextWithSpecifiedGranularity(CursorGranularity.WORD, false /* shouldWrap */, true /* shouldScroll */, true /* useInputFocusAsPivotIfEmpty */, InputModeManager.INPUT_MODE_TOUCH)); // Navigate backward by word. startRecordingUtterances(); assertTrue(mCursorController.previousWithSpecifiedGranularity(CursorGranularity.WORD, false /* shouldWrap */, true /* shouldScroll */, true /* useInputFocusAsPivotIfEmpty */, InputModeManager.INPUT_MODE_TOUCH)); waitForAccessibilityIdleSync(); stopRecordingAndAssertUtterance("page"); startRecordingUtterances(); assertTrue(mCursorController.previousWithSpecifiedGranularity(CursorGranularity.WORD, false /* shouldWrap */, true /* shouldScroll */, true /* useInputFocusAsPivotIfEmpty */, InputModeManager.INPUT_MODE_TOUCH)); waitForAccessibilityIdleSync(); stopRecordingAndAssertUtterance("of"); // Navigate forward and backward by character. startRecordingUtterances(); assertTrue(mCursorController.nextWithSpecifiedGranularity(CursorGranularity.CHARACTER, false /* shouldWrap */, true /* shouldScroll */, true /* useInputFocusAsPivotIfEmpty */, InputModeManager.INPUT_MODE_TOUCH)); waitForAccessibilityIdleSync(); stopRecordingAndAssertUtterance("o"); startRecordingUtterances(); assertTrue(mCursorController.nextWithSpecifiedGranularity(CursorGranularity.CHARACTER, false /* shouldWrap */, true /* shouldScroll */, true /* useInputFocusAsPivotIfEmpty */, InputModeManager.INPUT_MODE_TOUCH)); waitForAccessibilityIdleSync(); stopRecordingAndAssertUtterance("f"); startRecordingUtterances(); assertTrue(mCursorController.previousWithSpecifiedGranularity(CursorGranularity.CHARACTER, false /* shouldWrap */, true /* shouldScroll */, true /* useInputFocusAsPivotIfEmpty */, InputModeManager.INPUT_MODE_TOUCH)); waitForAccessibilityIdleSync(); stopRecordingAndAssertUtterance("f"); // Try to go to next item and confirm that above operation doesn't affect to this. assertTrue(mCursorController.next(true /* shouldWrap */, true /* shouldScroll */, true /* useInputFocusAsPivotIfEmpty */, InputModeManager.INPUT_MODE_TOUCH)); waitForAccessibilityIdleSync(); assertEquals(getNodeForId(R.id.button_square), mCursorController.getCursor()); } @MediumTest public void testNext_noWrap_noScroll_doUseInputFocus() { setContentView(R.layout.cursor_test); // Assign input focus to the button, but don't touch accessibility focus. getInstrumentation().runOnMainSync(new Runnable() { @Override public void run() { View squareButton = getViewForId(R.id.button_square); squareButton.requestFocus(); } }); getInstrumentation().waitForIdleSync(); waitForAccessibilityIdleSync(); // This is the expected traversal: // (Initial focus) button_square - has input focus // 1. check_me AccessibilityNodeInfoCompat checkBox = getNodeForId(R.id.check_me); mCursorController.next(false, false, true, InputModeManager.INPUT_MODE_TOUCH); assertEquals(checkBox, mCursorController.getCursor()); } @MediumTest public void testNext_horizontal() { setContentView(R.layout.cursor_horizontal_test); AccessibilityNodeInfoCompat firstLabel = getNodeForId(R.id.text_first_item); AccessibilityNodeInfoCompat lastLabel = getNodeForId(R.id.text_last_item); AccessibilityNodeInfoCompat horizScroller = getNodeForId(R.id.horiz_scroller); mCursorController.setCursor(firstLabel); waitForAccessibilityIdleSync(); // This is the expected traversal: // (Initial focus) text_first_item // 1-7. horiz_scroller - all items // 8. text_last_item final int traversals = 8; List<AccessibilityNodeInfoCompat> nodes = navigate(TraversalStrategy.SEARCH_FOCUS_FORWARD, traversals, true, true, true); assertHasParent(horizScroller, nodes.subList(0, traversals - 1)); assertEquals(lastLabel, nodes.get(traversals - 1)); } @MediumTest public void testPrevious_doWrap_doScroll_doUseInputFocus() { setContentView(R.layout.cursor_test); // Scroll teams list to the last item. getInstrumentation().runOnMainSync(new Runnable() { @Override public void run() { ListView teamsListView = (ListView) getViewForId(R.id.teams_list); teamsListView.setSelection(teamsListView.getCount() - 1); } }); getInstrumentation().waitForIdleSync(); // Put input focus on button AFTER the teams list is done updating. getInstrumentation().runOnMainSync(new Runnable() { @Override public void run() { View squareButton = getViewForId(R.id.button_square); squareButton.requestFocus(); } }); getInstrumentation().waitForIdleSync(); waitForAccessibilityIdleSync(); AccessibilityNodeInfoCompat teamsList = getNodeForId(R.id.teams_list); AccessibilityNodeInfoCompat checkBox = getNodeForId(R.id.check_me); AccessibilityNodeInfoCompat firstLabel = getNodeForId(R.id.text_first_item); AccessibilityNodeInfoCompat lastLabel = getNodeForId(R.id.text_last_item); // This is the expected traversal: // (Initial focus) button_square - has input focus // 1. text_first_item // 2. text_first_item - first previous() pauses on node // 3. text_last_item - second previous() wraps around // 4-12: teams_list items // 13. check_me final int traversals = 13; List<AccessibilityNodeInfoCompat> nodes = navigate(TraversalStrategy.SEARCH_FOCUS_BACKWARD, traversals, true, true, true); assertEquals(firstLabel, nodes.get(0)); assertEquals(firstLabel, nodes.get(1)); assertEquals(lastLabel, nodes.get(2)); assertHasParent(teamsList, nodes.subList(3, traversals - 1)); assertEquals(checkBox, nodes.get(traversals - 1)); } @MediumTest public void testPrevious_horizontal() { setContentView(R.layout.cursor_horizontal_test); // Scroll to the very right to reach the last items before beginning test. getInstrumentation().runOnMainSync(new Runnable() { @Override public void run() { HorizontalScrollView scroller = (HorizontalScrollView) getViewForId(R.id.horiz_scroller); scroller.fullScroll(View.FOCUS_RIGHT); } }); getInstrumentation().waitForIdleSync(); waitForAccessibilityIdleSync(); AccessibilityNodeInfoCompat firstLabel = getNodeForId(R.id.text_first_item); AccessibilityNodeInfoCompat lastLabel = getNodeForId(R.id.text_last_item); AccessibilityNodeInfoCompat horizScroller = getNodeForId(R.id.horiz_scroller); mCursorController.setCursor(lastLabel); waitForAccessibilityIdleSync(); // This is the expected traversal: // (Initial focus) text_last_item // 1-7. horiz_scroller - all items // 8. text_first_item final int traversals = 8; List<AccessibilityNodeInfoCompat> nodes = navigate(TraversalStrategy.SEARCH_FOCUS_BACKWARD, traversals, true, true, true); assertHasParent(horizScroller, nodes.subList(0, traversals - 1)); assertEquals(firstLabel, nodes.get(traversals - 1)); } @MediumTest public void testDown_noWrap_doScroll_noUseInputFocus() { if (!checkApiLevelSupportsDirectional()) { return; } setContentView(R.layout.cursor_test); AccessibilityNodeInfoCompat teamsList = getNodeForId(R.id.teams_list); AccessibilityNodeInfoCompat firstLabel = getNodeForId(R.id.text_first_item); AccessibilityNodeInfoCompat squareButton = getNodeForId(R.id.button_square); AccessibilityNodeInfoCompat checkBox = getNodeForId(R.id.check_me); AccessibilityNodeInfoCompat lastLabel = getNodeForId(R.id.text_last_item); mCursorController.setCursor(firstLabel); waitForAccessibilityIdleSync(); // This is the expected traversal: // (Initial focus) text_first_item // 1. button_square // 2. check_me // 3-N. teams_list - visible items only // N. text_last_item final int teamsVisibleCount = teamsList.getChildCount(); final int traversals = 3 + teamsVisibleCount; // 3 non-list items + N visible list items. List<AccessibilityNodeInfoCompat> nodes = navigate(TraversalStrategy.SEARCH_FOCUS_DOWN, traversals, false, false, false); assertEquals(squareButton, nodes.get(0)); assertEquals(checkBox, nodes.get(1)); assertHasParent(teamsList, nodes.subList(2, traversals - 1)); assertEquals(lastLabel, nodes.get(traversals - 1)); } @MediumTest public void testDown_horizontal() { if (!checkApiLevelSupportsDirectional()) { return; } setContentView(R.layout.cursor_horizontal_test); AccessibilityNodeInfoCompat firstLabel = getNodeForId(R.id.text_first_item); AccessibilityNodeInfoCompat lastLabel = getNodeForId(R.id.text_last_item); AccessibilityNodeInfoCompat horizScroller = getNodeForId(R.id.horiz_scroller); mCursorController.setCursor(firstLabel); waitForAccessibilityIdleSync(); // This is the expected traversal: // (Initial focus) text_first_item // 1. horiz_scroller - a single item (doesn't really matter which specifically) // 2. text_last_item // 3. text_last_item - first down() should pause on node // 4. text_first_item - second down() should wrap around final int traversals = 4; List<AccessibilityNodeInfoCompat> nodes = navigate(TraversalStrategy.SEARCH_FOCUS_DOWN, traversals, true, true, true); assertHasParent(horizScroller, nodes.subList(0, 1)); assertEquals(lastLabel, nodes.get(1)); assertEquals(lastLabel, nodes.get(2)); assertEquals(firstLabel, nodes.get(3)); } @MediumTest public void testUp_doWrap_doScroll_doUseInputFocus() { if (!checkApiLevelSupportsDirectional()) { return; } setContentView(R.layout.cursor_test); // Scroll teams list to the last item. getInstrumentation().runOnMainSync(new Runnable() { @Override public void run() { ListView teamsListView = (ListView) getViewForId(R.id.teams_list); teamsListView.setSelection(teamsListView.getCount() - 1); } }); getInstrumentation().waitForIdleSync(); // Put input focus on button AFTER the teams list is done updating. getInstrumentation().runOnMainSync(new Runnable() { @Override public void run() { View squareButton = getViewForId(R.id.button_square); squareButton.requestFocus(); } }); getInstrumentation().waitForIdleSync(); waitForAccessibilityIdleSync(); AccessibilityNodeInfoCompat teamsList = getNodeForId(R.id.teams_list); AccessibilityNodeInfoCompat checkBox = getNodeForId(R.id.check_me); AccessibilityNodeInfoCompat firstLabel = getNodeForId(R.id.text_first_item); AccessibilityNodeInfoCompat lastLabel = getNodeForId(R.id.text_last_item); // This is the expected traversal: // (Initial focus) button_square - has input focus // 1. text_first_item // 2. text_first_item - first previous() pauses on node // 3. text_last_item - second previous() wraps around // 4-12: teams_list items // 13. check_me final int traversals = 13; List<AccessibilityNodeInfoCompat> nodes = navigate(TraversalStrategy.SEARCH_FOCUS_UP, traversals, true, true, true); assertEquals(firstLabel, nodes.get(0)); assertEquals(firstLabel, nodes.get(1)); assertEquals(lastLabel, nodes.get(2)); assertHasParent(teamsList, nodes.subList(3, traversals - 1)); assertEquals(checkBox, nodes.get(traversals - 1)); } @MediumTest public void testRight_horizontal() { if (!checkApiLevelSupportsDirectional()) { return; } setContentView(R.layout.cursor_horizontal_test); AccessibilityNodeInfoCompat horizScroller = getNodeForId(R.id.horiz_scroller); mCursorController.setCursor(horizScroller.getChild(0)); waitForAccessibilityIdleSync(); // This is the expected traversal: // (Initial focus) horiz_scroller - first item. // 1-20. horiz_scroller - we should wrap around twice but keep hitting horiz_scroller final int traversals = 20; // Arbitrary big number. Enough to wrap around a few times. List<AccessibilityNodeInfoCompat> nodes = navigate(TraversalStrategy.SEARCH_FOCUS_RIGHT, traversals, true, true, true); assertHasParent(horizScroller, nodes); assertNotEquals(nodes.get(0), nodes.get(1)); // We should get different nodes! } @MediumTest public void testSetGranularity() { setContentView(R.layout.text_activity); AccessibilityNodeInfoCompat usernameLabel = getNodeForId(R.id.username_label); mCursorController.setCursor(usernameLabel); mCursorController.setGranularity(CursorGranularity.CHARACTER, false); waitForAccessibilityIdleSync(); startRecordingUtterances(); // It should read [username] as "u", "s", "e", "r", etc. // Read up until the seventh letter ("m"). for (int i = 0; i < 7; ++i) { mCursorController.next(false, false, false, InputModeManager.INPUT_MODE_TOUCH); waitForAccessibilityIdleSync(); } // Need to match "m" exactly (we want "m" by itself and not "username" which contains "m"). final CharSequenceFilter textFilter = new CharSequenceFilter().addMatchesPattern("m", 0); UtteranceFilter utteranceFilter = new UtteranceFilter().addTextFilter(textFilter); final Utterance utterance = stopRecordingUtterancesAfterMatch(utteranceFilter); assertNotNull("Saw matching utterance", utterance); } @MediumTest public void testSetGranularity_thenNavigateAway() { setContentView(R.layout.text_activity); final TextView usernameView = (TextView) getViewForId(R.id.username); getInstrumentation().runOnMainSync(new Runnable() { @Override public void run() { usernameView.setText("potatoes"); } }); getInstrumentation().waitForIdleSync(); waitForAccessibilityIdleSync(); AccessibilityNodeInfoCompat usernameLabel = getNodeForId(R.id.username_label); mCursorController.setCursor(usernameLabel); mCursorController.setGranularity(CursorGranularity.CHARACTER, false); waitForAccessibilityIdleSync(); startRecordingUtterances(); // First read [username] at character granularity then begin reading [potatoes]. // It should read [username] as "u", "s", "e", "r", etc. // 1-8: "u", "s", "e", "r"... // 9. (indication that we've reached the end) // 10. "p" for (int i = 0; i < 10; ++i) { mCursorController.next(false, false, false, InputModeManager.INPUT_MODE_TOUCH); waitForAccessibilityIdleSync(); } // We need to match an utterance with exactly the string "p". final CharSequenceFilter textFilter = new CharSequenceFilter().addMatchesPattern("p", 0); UtteranceFilter utteranceFilter = new UtteranceFilter().addTextFilter(textFilter); final Utterance utterance = stopRecordingUtterancesAfterMatch(utteranceFilter); assertNotNull("Saw matching utterance", utterance); } @MediumTest public void testSetSelectionModeActive() { setContentView(R.layout.text_activity); final TextView usernameView = (TextView) getViewForId(R.id.username); getInstrumentation().runOnMainSync(new Runnable() { @Override public void run() { usernameView.setText("abcdefghijklmnop"); } }); getInstrumentation().waitForIdleSync(); waitForAccessibilityIdleSync(); AccessibilityNodeInfoCompat username = getNodeForId(R.id.username); mCursorController.setCursor(username); mCursorController.setGranularity(CursorGranularity.CHARACTER, false); waitForAccessibilityIdleSync(); // Move cursor between "c" and "d". for (int i = 0; i < 3; ++i) { mCursorController.next(false, false, false, InputModeManager.INPUT_MODE_TOUCH); waitForAccessibilityIdleSync(); } mCursorController.setSelectionModeActive(username, true); waitForAccessibilityIdleSync(); // Select five characters "defgh". for (int i = 0; i < 5; ++i) { mCursorController.next(false, false, false, InputModeManager.INPUT_MODE_TOUCH); waitForAccessibilityIdleSync(); } assertEquals(3, usernameView.getSelectionStart()); assertEquals(8, usernameView.getSelectionEnd()); // 8-3 = 5 char selection. } @MediumTest public void testAddGranularityListener() { setContentView(R.layout.text_activity); GranularityChangeListener listener = new GranularityChangeListener(); mCursorController.addGranularityListener(listener); AccessibilityNodeInfoCompat usernameLabel = getNodeForId(R.id.username_label); mCursorController.setCursor(usernameLabel); mCursorController.setGranularity(CursorGranularity.CHARACTER, false); waitForAccessibilityIdleSync(); assertEquals(1, listener.count); mCursorController.setGranularity(CursorGranularity.WORD, false); waitForAccessibilityIdleSync(); assertEquals(2, listener.count); } @MediumTest public void testAddScrollListener() { setContentView(R.layout.cursor_test); ScrollListener listener = new ScrollListener(); mCursorController.addScrollListener(listener); AccessibilityNodeInfoCompat teamsList = getNodeForId(R.id.teams_list); AccessibilityNodeInfoCompat teamItem = teamsList.getChild(0); mCursorController.setCursor(teamItem); waitForAccessibilityIdleSync(); mCursorController.more(); // Causes a downwards scroll. waitForAccessibilityIdleSync(); assertEquals(1, listener.count); } @MediumTest public void testNextPrevious_web() { setContentView(R.layout.cursor_web_test); WebAccessibilityDelegate delegate1 = new WebAccessibilityDelegate(true /* next */, false /* previous */); View webElement1 = getViewForId(R.id.web_element_1); webElement1.setAccessibilityDelegate(delegate1); AccessibilityNodeInfoCompat webNode1 = getNodeForView(webElement1); WebAccessibilityDelegate delegate2 = new WebAccessibilityDelegate(false /* next */, true /* previous */); View webElement2 = getViewForId(R.id.web_element_2); webElement2.setAccessibilityDelegate(delegate2); AccessibilityNodeInfoCompat webNode2 = getNodeForView(webElement2); AccessibilityNodeInfoCompat nativeNode = getNodeForId(R.id.native_element); // Start at web element 1. mCursorController.setCursor(webNode1); waitForAccessibilityIdleSync(); // Navigate to next web element. We verify that CursorController doesn't move cursor. mCursorController.next(false /* wrap */, false /* scroll */, false /* useInput */, InputModeManager.INPUT_MODE_TOUCH); waitForAccessibilityIdleSync(); assertTrue(delegate1.didPerformNextHtmlAction()); assertEquals(webNode1, mCursorController.getCursor()); // Set a11y focus to web element 2 manually using the CursorController. mCursorController.setCursor(webNode2); waitForAccessibilityIdleSync(); // Navigate to previous web element. We verify that CursorController doesn't move cursor. mCursorController.previous(false /* wrap */, false /* scroll */, false /* useInput */, InputModeManager.INPUT_MODE_TOUCH); waitForAccessibilityIdleSync(); assertTrue(delegate2.didPerformPreviousHtmlAction()); assertEquals(webNode2, mCursorController.getCursor()); // We're still at web element 2. Try to move to the native element. // We verify that the CursorController should move the cursor in this case. mCursorController.next(false /* wrap */, false /* scroll */, false /* useInput */, InputModeManager.INPUT_MODE_TOUCH); waitForAccessibilityIdleSync(); assertEquals(nativeNode, mCursorController.getCursor()); } @MediumTest public void testHitEdgeAnnouncement_web() { setContentView(R.layout.hit_edge_web_test); final WebView webView = (WebView) getViewForId(R.id.web_view); getInstrumentation().runOnMainSync(new Runnable() { @Override public void run() { loadWebViewFromResource(webView, R.raw.simple_page_webview); } }); getInstrumentation().waitForIdleSync(); waitForAccessibilityIdleSync(); AccessibilityNodeInfoCompat wholeWeb = getNodeForId(R.id.web_view); mCursorController.setCursor(wholeWeb); waitForAccessibilityIdleSync(); mCursorController.next(false /* wrap */, false /* scroll */, false /* useInput */, InputModeManager.INPUT_MODE_TOUCH); waitForAccessibilityIdleSync(); mCursorController.setGranularity(CursorGranularity.WEB_SECTION, true); waitForAccessibilityIdleSync(); //Move cursor to Heading 1 mCursorController.next(false /* wrap */, false /* scroll */, false /* useInput */, InputModeManager.INPUT_MODE_TOUCH); waitForAccessibilityIdleSync(); //Try to move to the next element. //It should notify the user no next heading or landmark. startRecordingRawSpeech(); mCursorController.next(false /* wrap */, false /* scroll */, false /* useInput */, InputModeManager.INPUT_MODE_TOUCH); waitForAccessibilityIdleSync(); stopRecordingAndAssertRawSpeech("No next heading or landmark"); } @MediumTest public void testNotPastLastHeading_web() { setContentView(R.layout.hit_edge_web_test); final WebView webView = (WebView) getViewForId(R.id.web_view); getInstrumentation().runOnMainSync(new Runnable() { @Override public void run() { loadWebViewFromResource(webView, R.raw.simple_page_webview); } }); getInstrumentation().waitForIdleSync(); waitForAccessibilityIdleSync(); AccessibilityNodeInfoCompat wholeWeb = getNodeForId(R.id.web_view); mCursorController.setCursor(wholeWeb); waitForAccessibilityIdleSync(); mCursorController.next(false /* wrap */, false /* scroll */, false /* useInput */, InputModeManager.INPUT_MODE_TOUCH); waitForAccessibilityIdleSync(); mCursorController.setGranularity(CursorGranularity.WEB_SECTION, true); waitForAccessibilityIdleSync(); //Move cursor to Heading 1 mCursorController.next(false /* wrap */, false /* scroll */, false /* useInput */, InputModeManager.INPUT_MODE_TOUCH); waitForAccessibilityIdleSync(); AccessibilityNodeInfoCompat heading = mCursorController.getCursor(); //Try to move to the next element. //It should stay on the last Heading. mCursorController.next(false /* wrap */, false /* scroll */, false /* useInput */, InputModeManager.INPUT_MODE_TOUCH); waitForAccessibilityIdleSync(); assertEquals(heading, mCursorController.getCursor()); //Try again to move to the next element. //It should stay on the last Heading. mCursorController.next(false /* wrap */, false /* scroll */, false /* useInput */, InputModeManager.INPUT_MODE_TOUCH); waitForAccessibilityIdleSync(); assertEquals(heading, mCursorController.getCursor()); } private void loadWebViewFromResource(WebView webView, int resourceId) { InputStream inputStream = getActivity().getResources() .openRawResource(resourceId); BufferedReader br = new BufferedReader(new InputStreamReader(inputStream)); String webContent = ""; String tmp = ""; try { while (tmp != null) { webContent += tmp; tmp = br.readLine(); } } catch (IOException e) { Log.e(CursorControllerAppTest.this.getName(), "Cannot read from file."); } webView.loadData(webContent, "text/html", null); } private List<AccessibilityNodeInfoCompat> navigate( @TraversalStrategy.SearchDirection int direction, int numberTraversals, boolean wrap, boolean scroll, boolean useInputFocus) { List<AccessibilityNodeInfoCompat> nodesTraversed = new ArrayList<>(); for (int i = 0; i < numberTraversals; ++i) { switch (direction) { case TraversalStrategy.SEARCH_FOCUS_FORWARD: mCursorController.next(wrap, scroll, useInputFocus, InputModeManager.INPUT_MODE_TOUCH); break; case TraversalStrategy.SEARCH_FOCUS_BACKWARD: mCursorController.previous(wrap, scroll, useInputFocus, InputModeManager.INPUT_MODE_TOUCH); break; case TraversalStrategy.SEARCH_FOCUS_LEFT: mCursorController.left(wrap, scroll, useInputFocus, InputModeManager.INPUT_MODE_TOUCH); break; case TraversalStrategy.SEARCH_FOCUS_RIGHT: mCursorController.right(wrap, scroll, useInputFocus, InputModeManager.INPUT_MODE_TOUCH); break; case TraversalStrategy.SEARCH_FOCUS_UP: mCursorController.up(wrap, scroll, useInputFocus, InputModeManager.INPUT_MODE_TOUCH); break; case TraversalStrategy.SEARCH_FOCUS_DOWN: mCursorController.down(wrap, scroll, useInputFocus, InputModeManager.INPUT_MODE_TOUCH); break; default: throw new IllegalArgumentException("direction must be a SearchDirection"); } waitForAccessibilityIdleSync(); AccessibilityNodeInfoCompat node = AccessibilityNodeInfoCompat.obtain( mCursorController.getCursor()); nodesTraversed.add(node); mObtainedNodes.add(node); } return nodesTraversed; } private void assertHasParent(AccessibilityNodeInfoCompat parent, List<AccessibilityNodeInfoCompat> items) { for (AccessibilityNodeInfoCompat node : items) { assertEquals(parent, node.getParent()); } } private boolean checkApiLevelSupportsDirectional() { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M; } private class GranularityChangeListener implements CursorController.GranularityChangeListener { public int count = 0; @Override public void onGranularityChanged(CursorGranularity granularity) { count++; } } private class ScrollListener implements CursorController.ScrollListener { public int count = 0; @Override public void onScroll(AccessibilityNodeInfoCompat scrolledNode, int action, boolean auto) { count++; } } private class WebAccessibilityDelegate extends View.AccessibilityDelegate { private final boolean mHasNext; private final boolean mHasPrevious; private int mActionsPerformed = 0; public WebAccessibilityDelegate(boolean hasNext, boolean hasPrevious) { mHasNext = hasNext; mHasPrevious = hasPrevious; } @Override public boolean performAccessibilityAction(View host, int action, Bundle args) { mActionsPerformed = mActionsPerformed | action; if (action == AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT && mHasNext) { return true; } if (action == AccessibilityNodeInfo.ACTION_PREVIOUS_HTML_ELEMENT && mHasPrevious) { return true; } return super.performAccessibilityAction(host, action, args); } @Override public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfo(host, info); if (mHasNext) { info.addAction(AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT); } if (mHasPrevious) { info.addAction(AccessibilityNodeInfo.ACTION_PREVIOUS_HTML_ELEMENT); } } public boolean didPerformNextHtmlAction() { return (mActionsPerformed & AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT) != 0; } public boolean didPerformPreviousHtmlAction() { return (mActionsPerformed & AccessibilityNodeInfo.ACTION_PREVIOUS_HTML_ELEMENT) != 0; } } }